fix(asr): StepFun 实时转写不再随机白等 12 秒(13% 的听写中招) - #870
Merged
H-Chris233 merged 2 commits intoAug 1, 2026
Conversation
真机日志(1059 次听写)统计「松手 → 出结果」耗时:中位数 0.87s,但有 117 次 精确落在 12.00~12.97s,8~12s 之间一次都没有 —— 是硬超时兜底,不是网络抖动。 按 provider 拆分后只有 StepFun 中招:117/887 = 13.2%,bailian / qwen3-realtime / siliconflow 合计 0/172。三条语音链路(听写、语音问答、重新转录)共用同一个 send_last_frame,全都受影响,且全程不打任何日志。 根因在收尾状态机: 1. contains_non_silent_pcm 用「任意字节 != 0」判静音。麦克风底噪(实测 RMS≈0.0008)每个采样都非零,于是尾帧永远被判成「有真实语音」,把 last_non_silent_audio_written_at 刷到最后一个 completed 之后 —— has_audio_after_last_completed 从此恒为真。 2. 宽限任务只在 FINISH_GRACE 到点查一次。这一次因上述原因不通过,就再没有第二 次机会;而尾帧写出后客户端不再发音频,服务端也不会再吐事件(松手前停顿 ≥VAD 阈值时,最后一段的 completed 早在尾帧之前就到了),于是一路干等到 FINAL_RESULT_TIMEOUT。12s 后带着 partial 成功返回,所以文字是对的、也没报错。 对应三处修改: - contains_non_silent_pcm 改成按振幅判(>1% 满量程、且至少 8 个采样,避开单点 尖峰),并且只对尾帧的真实录音部分求值,补的静音不参与。 - 宽限任务改成有界轮询(100ms 一次)。收尾前提保持不变 —— 「无未关句段 + 最后 一个 completed 之后没有未确认的真实音频」那条保护是对的,不为修卡顿而丢; 新增 FINISH_HARD_DEADLINE=3s 兜住服务端始终不关句段的情况。 - 12s 兜底分支补 log::warn!,这种事不该在日志里完全隐身。 行为学佐证:卡顿会话录音末段峰值 RMS<0.005(已静音)的占 52%,正常会话仅 16%; 卡顿会话原始转写 66% 以「嗯。」结尾,正常会话仅 8% —— 都指向「说完停顿一下再 松手」这个触发条件。 测试:三条新增测试在修复前均失败(两条重现 12.00s 卡死),修复后 19/19 通过。 既有的 non_silent_audio_waits_for_delayed_vad_before_finishing 保持通过,只换了 夹具([1u8; N] 在新判据下属于底噪,改用语音级振幅)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
PR Reviewer Guide 🔍(Review updated until commit 491be37)Here are some key observations to aid the review process:
|
Collaborator
|
大松你真牛逼,测试1059次,太牛逼了 |
Contributor
|
Persistent review updated to latest commit 491be37 |
Contributor
Author
项目的重度用户啊,每天我至少要用 openless 说一个小时的话 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
User description
现象
用 StepFun 实时转写时,随机有一部分听写在松手后要干等 12 秒才出结果。文字是对的,也不报错,日志里一条痕迹都没有。
数据
真机日志 1059 次听写,统计「松手(
cpal Stream dropped)→ 出结果(polish dispatch)」耗时:中位数 0.87s,8~12 秒之间一次都没有 —— 典型的硬超时兜底,不是网络抖动或模型慢。
按 provider 拆分,只有 StepFun 中招:
原因是其他三家都有协议级 finish 信号(Bailian 的
finish-task、Qwen 的 Finish 事件、Volcengine 的NegativeSequence末帧),服务端会回一个确定的终态;StepFun 没有(模块注释已记录:session.finish被拒、server_vad 下commit被静默忽略),只能靠客户端补静音逼 VAD 关段 + 自建状态机判定收尾 —— 出问题的正是这套判定。影响面不止听写:
send_last_frame被听写、语音问答、重新转录三条链路共用。根因
两处叠加:
1.
contains_non_silent_pcm用「任意字节 != 0」判静音。 麦克风底噪(实测 RMS≈0.0008)每个采样都非零,于是任何一帧都算「有真实语音」。尾帧因此把last_non_silent_audio_written_at刷到最后一个completed之后,has_audio_after_last_completed从此恒为真。2. 宽限任务只在
FINISH_GRACE到点查一次。 这一次因上述原因不通过,就再没有第二次机会。而尾帧写出后客户端不再发音频、服务端也不会再吐事件(松手前停顿 ≥VAD 阈值时,最后一段的completed早在尾帧之前就到了),于是一路干等到FINAL_RESULT_TIMEOUT。12s 后走finish_with_partial_or_error,带着已有文本成功返回 —— 所以文字对、无报错、无日志。行为学佐证,都指向「说完停顿一下再松手」这个触发条件:
改动
contains_non_silent_pcm改成按振幅判(> 1% 满量程,且至少 8 个采样超阈值以避开键盘/电流的单点尖峰);并且只对尾帧的真实录音部分求值,补的 700ms 静音不参与判定。completed之后没有未确认的真实音频」那条保护是对的,不为修卡顿而丢掉;新增FINISH_HARD_DEADLINE = 3s兜住服务端始终不关最后一个句段的情况,把最坏等待从 12s 压到 3s。log::warn!。这种事不该在日志里完全隐身。纯客户端时序改动,发给 StepFun 的字节流完全不变,无接口兼容风险。
测试
新增 3 条,修复前全部失败(其中两条重现 12.00s 卡死):
silence_detection_ignores_room_tone_but_catches_speech— 底噪/语音/纯静音/单点尖峰四种输入finishes_promptly_when_last_completed_arrived_before_the_tail— 本 issue 的主路径;测试内显式等到last_completed_at落地才写尾帧,钉死「completed 先于尾帧」这个时序,否则会走健康快路径复现不出来hard_deadline_bounds_the_wait_when_segment_never_closes— 服务端只发 speech_started + delta、永不 completed,验证 3s 硬上限生效且未 completed 的 interim 尾巴仍被带出、不丢字既有的
non_silent_audio_waits_for_delayed_vad_before_finishing保持通过(它保证的「有未确认语音时不能提前收尾」是被有意保留的),只换了夹具:[1u8; N]在新判据下属于底噪,改用语音级振幅。cargo check --tests通过;cargo test --lib asr::stepfun_realtime19/19 通过。🤖 Generated with Claude Code
PR Type
Bug fix, Tests
Description
修复 StepFun 实时转写约 13% 听写随机白等 12 秒的问题
根因:
contains_non_silent_pcm把底噪当语音,导致宽限检查一次不通过且无重试修复:按振幅判静音、宽限期轮询复查,并增加 3 秒硬上限兜底
新增回归测试覆盖“completed 早于尾帧”和“句段永不关闭”两种场景
Diagram Walkthrough
flowchart LR A["尾帧写出"] --> B["每100ms轮询收尾条件"] B -->|"宽限内无未关句段且无新语音"| C["finish_success"] B -->|"超过3秒硬上限"| D{"有可返回文本?"} D -->|"是"| C D -->|"否"| E["等待至 FINAL_RESULT_TIMEOUT 后显式报错"]File Walkthrough
stepfun_realtime.rs
修复 StepFun 收尾静音误判与 12 秒超时卡顿openless-all/app/src-tauri/src/asr/stepfun_realtime.rs
contains_non_silent_pcm从“任一字节非零”改为按振幅阈值与最少采样数判断,避免底噪误判FINISH_POLL_INTERVAL轮询,并增加FINISH_HARD_DEADLINE(3秒)强制收尾兜底
has_transcript、should_force_finish_at_hard_deadline,统一部分结果返回与硬截止行为